/*Copyright 2014 Rajesh Putta

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 * 
 */

package com.codesnippets4all.xss.config.handlers;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.codesnippets4all.xss.exceptions.XSSConfigLoadingException;

/*
 * This component uses SAX Parser to parse xss-config.xml configuration file
 */
public class XSSConfigHandler extends DefaultHandler {

	private static XSSConfigHandler configHandler = null;
	private List<String> parameterSet = null;
	private List<String> headerSet = null;
	private List<Pattern> patternList = new ArrayList<Pattern>();

	private StringBuilder content = new StringBuilder();
	private String flags = null;

	// create an XML configuration file with all the identified patterns to be
	// applied on the request parameters
	private String configFile = "com/codesnippets4all/xss/config/xss-config.xml";

	public void setConfigFile(String configFile) {
		this.configFile = configFile;
	}

	public String getConfigFile() {
		return configFile;
	}

	public static XSSConfigHandler getInstance() {
		if (configHandler == null) {
			configHandler = new XSSConfigHandler();
		}

		return configHandler;
	}

	private XSSConfigHandler() {
	}
	
	public void initialize() {
		InputStream stream=null;
		try {
			SAXParserFactory factory = SAXParserFactory.newInstance();
			SAXParser parser = factory.newSAXParser();
			stream = createStreamForConfig();
			parser.parse(stream, this);
		} catch (Exception e) {
			throw new XSSConfigLoadingException(e);
		}
		finally{
			if(stream!=null)
			{
				try {
					stream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	// This method try to read the input configuration file with the help of
	// different levels of Class Loaders

	private InputStream createStreamForConfig() {
		if (this.configFile == null || this.configFile.trim().equals(""))
			throw new IllegalStateException(
					"config file to be parsed is not configured");

		InputStream stream = this.getClass().getResourceAsStream(
				this.configFile);

		if (stream == null)
			stream = Thread.currentThread().getContextClassLoader()
					.getResourceAsStream(this.configFile);

		if (stream == null)
			stream = ClassLoader.getSystemResourceAsStream(this.configFile);

		return stream;
	}

	@Override
	public void startDocument() throws SAXException {
		super.startDocument();
	}

	@Override
	public void startElement(String uri, String localName, String qName,
			Attributes attributes) throws SAXException {
		if (qName.equals("xss-parameters")) {
			parameterSet = new ArrayList<String>();
		} else if (qName.equals("xss-pattern")) {
			this.flags = attributes.getValue("flags");
		} else if (qName.equals("xss-headers")) {
			headerSet = new ArrayList<String>();
		}

		// recycle content instance to hold next immediate characters
		content.delete(0, content.length());
	}

	@Override
	public void characters(char[] ch, int start, int length)
			throws SAXException {
		content.append(ch, start, length);
	}

	@Override
	public void endElement(String uri, String localName, String qName)
			throws SAXException {
		if (qName.equals("parameter")) {
			String temp = content.toString().trim();

			if (!temp.equals(""))
				parameterSet.add(temp);
		} else if (qName.equals("header")) {
			String temp = content.toString().trim();

			if (!temp.equals("")) {
				headerSet.add(temp);
			}
		} else if (qName.equals("xss-pattern")) {
			String patternStr = content.toString().trim();

			if (patternStr.equals(""))
				return;

			int finalFlag = 0x02;

			if (flags != null && !flags.trim().equals("")) {
				String[] flagArr = flags.split(",");

				for (int i = 0; i < flagArr.length; i++) {
					flagArr[i] = flagArr[i].trim();

					if (!flagArr[i].startsWith("Pattern.")) {
						throw new XSSConfigLoadingException(
								"Invalid Pattern configured under flags attribute");
					} else
						flagArr[i] = flagArr[i].substring("Pattern.".length());

					try {
						Field field = Pattern.class
								.getDeclaredField(flagArr[i]);

						finalFlag |= field.getInt(null);

					} catch (SecurityException e) {
						throw new XSSConfigLoadingException(
								"Invalid Pattern configured under flags attribute");
					} catch (NoSuchFieldException e) {
						throw new XSSConfigLoadingException(
								"Invalid Pattern configured under flags attribute");
					} catch (IllegalArgumentException e) {
						throw new XSSConfigLoadingException(
								"Invalid Pattern configured under flags attribute");
					} catch (IllegalAccessException e) {
						throw new XSSConfigLoadingException(
								"Invalid Pattern configured under flags attribute");
					}
				}
			}

			Pattern pattern = Pattern.compile(patternStr, finalFlag);

			patternList.add(pattern);
		}
	}

	@Override
	public void endDocument() throws SAXException {
		content.delete(0, content.length());

		if (this.parameterSet != null)
			Collections.sort(this.parameterSet);

		if (this.headerSet != null)
			Collections.sort(this.headerSet);
	}

	public List<Pattern> getPatterns() {
		return this.patternList;
	}

	public List<String> getParameters() {
		return this.parameterSet;
	}

	public List<String> getHeaders() {
		return this.headerSet;
	}
}